Spring cloud 限流及限流算法

限流的主要目的是防止过渡的消耗资源,从而来保障服务对所有用户的可用性,常见的限流措施如诸以限制并发数为目的的数据库连接池/线程池,以及瞬时并发数的限制,和限制每秒的平均速率都是最为常见的。而这些基本上都会在网关层上进行实现,如使用 nginx、zuul、gateway、openresty、kong 等方式进行限流。

以维基百科的话来说就是在计算机网络中,控制网络接口请求速率

如果没有进行限流的话,则有几率会被恶意提交,导致后续服务无法得到提供,从而达到拒绝服务攻击,而限流在高并发中,是一个必不可少的一个因素之一,通过限流发挥的作用从而保护用户的可用性,甚至防止一些恶意请求等问题的发生。

限流算法

计数器算法(固定窗口)

计数器算法是一个简单以及暴力的,他从第一个请求开始计数,在假设我们设置一个 QPS(每秒查询数为 100),一秒内请求超过了设定值,那么接下来的请求都会被拒绝,等过一秒后才可继续查询,这时会将计数恢复成0。

看似这个算法很好且可以解决一些简单的问题,但这个算法也存在的一些问题,如我在 100ms 内通过了 100 个请求,则后续的 900ms 内的请求都会被拒绝,因此这种现象也被称之为 “突刺现象”。虽然我们可以通过将单位时间设置的更短,但依旧无法解决核心的问题,这时就诞生了漏桶算法和令牌桶算法。

计数器算法(滑动窗口)

滑动窗口又是计数器算法的一个改进,主要解决了固定窗口上的 临界值 的问题,将一个计时器分为了诺干个小的计时器,这些计时器都是独立的,当请求时间大于第一个计时器最大时间时,那么将会向前平移一个计时器(同时将前一个计时器丢弃,然后将当前的计时器设置为第一个计时器)。

如上图中,窗口大小为 1s,那么每过 0.2ms就会删除前一个格子,从而向右移一个格子,滑动窗口最为主要的就是他的滑动,使得可以控制的更加细密,以此来解决临界值的问题,但滑动窗口和固定窗口都无法解决突刺现象这一问题。

漏桶算法

漏桶算法(Leaky Bucket)内主要有两个变量,分别为 桶的大小以及漏洞(孔的大小)的大小,可以将请求比喻水,当水倒入漏桶中,需要有一定的处理速度(孔的大小决定处理的速度),而桶的大小决定了可以容纳请求的大小,如果请求太大,则会导致漏桶装不下请求从而拒绝后续的请求。

令牌桶算法

令牌桶算法(Token Bucket)这种算法和我们在医院排队叫号比较相似,当请求到达的时候,会去令牌桶(由令牌工厂生产令牌)中取得一个令牌,之后等待响应。

这也达到了一定限流的目的,首先我们可以决定令牌工厂放入令牌的速率,也可以定时或者根据一些特定的规则来达到目的,从而达到增加/减少令牌的数量达到限流的效果。

限流的实现

Spring cloud Gateway 内置了限流工厂 RequestRateLimiterGatewayFilterFactory 底层使用 redis lua 脚本进行实现,因此需要添加 Consul、Gateway、Redis 的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis-reactive -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<version>2.5.2</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

之后编写我们的限流配置类,主要通过名称、地址、以及 API 进行限流三种,也可以根据不同的状况自定义配置(还有一个跟均 CPU 进行限流的?)

LimitConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.example.demo.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

/**
* 限流配置文件,根据名称、地址、API
*
* @author kunlun
* @date 2021/7/23
*/
@Component
public class LimitConfig {

@Bean
KeyResolver userKeyResolver() {
return exchange ->
Mono.just(exchange.getRequest().getQueryParams().
getFirst("user"));
}
@Primary
@Bean
KeyResolver ipKeyResolver() {
return exchange ->
Mono.just(exchange.getRequest().getRemoteAddress().
getHostName());
}

@Bean
KeyResolver apiKeyResolver() {
return exchange ->
Mono.just(exchange.getRequest().getPath().
value());
}
}
application.yml

最后通过配置文件 application.yml 来链接 consul、redis 以及配置路由规则和全局过滤等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
spring:
application:
name: GatewayLimit
redis:
host: localhost
port: 6379
database: 1
password: toor
cloud:
gateway:
routes:
- id: hey
uri: lb://service-provider/hey
predicates:
- Path=/hey
- Method=GET
filters:
- name: RequestRateLimiter # 用于确定当前请求是否继续(如果不允许则会返回 429
args:
key-resolver: '#{@ipKeyResolver}'
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充的平均速率
redis-rate-limiter.burstCapacity: 3 # 令牌桶总流量
discovery:
locator:
# 允许服务发现
enabled: true
consul:
host: localhost
port: 8500
discovery:
service-name: service-provider

server:
port: 8210

保证 redis、consul 等已经启动且可提供服务

端点

端点在 Spring cloud Gateway 中主要提供的作用就是方便开发或者运维人员进行调试,以此来获取过滤器列表、路由列表、路由信息、刷新路由信息以及添加\删除路由等操作,这依赖于 Spring boot Actuator ,需要注意的是这些端点都会在 /actuator/gateway 路径下,分别提供对应的服务:

Id Name Method Info Uri
1 globalfilters GET 展示所有全局过滤器 /actuator/gateway/globalfilters
2 routefilters GET 展示所有过滤器工厂 /actuator/gateway/routefilters
3 refresh POST 清空路由缓存 /actuator/gateway/refresh
4 routes GET 获取路由列表 /actuator/gateway/routes
5 routes{name} GET 获取指定的路由信息 /actuator/gateway/routes/{name}
6 routes{name} POST 添加一个路由 /actuator/gateway/routes/{name}
7 routes{name} DELETE 移出一个路由 /actuator/gateway/routes/{name}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis-reactive -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<version>2.5.2</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.5.2</version>
</dependency>

.

1
2
3
4
5
6
……
management:
endpoints:
web:
exposure:
include: '*'

开启后,可以访问 http://localhost:8210/actuator/gateway/routes/hey 来进行测试是否可以该路由的信息,如果没有返回则可能是 Actuator 配置上有一点点的问题

management 中,默认只开启了两个端点,分别为 health、info 两个 因此我们可以通过上述配置开启全部的断点访问,更详细的可以查阅 Actuator API: https://cloud.spring.io/spring-cloud-gateway/multi/multi__actuator_api.html

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布